﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FinanSoft.Entities;
using FinanSoft.Utilities;
using System.Data.SqlClient;
using FinanSoft.BusinessLayer;
using FinanSoft.DataLayer;
using System.Drawing;

namespace FinanSoft.BusinessLayer
{
    public class ItemManager
    {
        #region Fields
        private bool _IsError;
        private String _ErrorMessage;
        private bool _definiteError;
        private static ItemManager _instance;
        private Dictionary<int, MeasurementUnit> _unidMedida;
        private Dictionary<int, Item> _items;

        #endregion

        #region Propiedades
        public bool IsError
        {
            get
            {
                bool temp = _IsError;
                _IsError = false;
                return temp;
            }
        }

        public String ErrorMessage
        {
            get
            {
                String temp = _ErrorMessage;
                _IsError = false;
                _ErrorMessage = "";
                return temp;
            }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor privado de la clase para permitir la implementacion del patron singleton
        /// </summary>
        private ItemManager()
        {
            _IsError = false;
            _ErrorMessage = "";
            List<MeasurementUnit> allMeasures = ItemDataManager.GetInstance().GetMeasuramentUnit();
            List<Item> allItems = ItemDataManager.GetInstance().GetItems();
            if (allMeasures == null)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_UM_READING;
            }
            else if (allItems == null)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_ARTICLE_READING;
            }
            else
            {
                _items = new Dictionary<int, Item>();
                _unidMedida = new Dictionary<int, MeasurementUnit>();
                
                foreach (MeasurementUnit measure in allMeasures)
                    _unidMedida.Add(measure.idUnidadMedida, measure);
                foreach (Item item in allItems)
                    _items.Add(item.Id, item);
                _IsError = false;
            }

        }        
        #endregion  

        #region Methods

        /// <summary>
        /// Agrega el detalle de la nueva bodega a todos los artículos
        /// </summary>
        /// <param name="newStore"></param>
        public void AddNewStore(StoreRoom newStore)
        {
            foreach (Item item in _items.Values)
                item.AddNewStoreRoom(newStore);
        }

        /// <summary>
        /// Metodo que obtienen la instancia existente de la clase. Si esta aun no ha sido instanciada, la instancia.
        /// </summary>
        public static ItemManager GetInstance()
        {
            if (_instance == null)
                _instance = new ItemManager();
            return _instance;
        }

        /// <summary>
        /// Devuelve el nuevo costo unitario del artículo para la bodega dada
        /// </summary>
        /// <param name="itemId">Id del articulo para el que entraron nuevas unidades</param>
        /// <param name="storeRoomId">Id de la bodega en que entraron</param>
        /// <param name="localAmount">Precio unitario en la moneda local</param>
        /// <param name="numberOfUnits">Cantidad de unidades que entraron</param>
        public bool UpdateUnitCost(int itemId, int storeRoomId, double localAmount, int numberOfUnits)
        {
            Item updatedItem = GetItem(itemId);
            ItemStoreRoomInfo storeInfo = updatedItem.GetStoreRoomInfo(storeRoomId);
            double newCost = Math.Round((storeInfo.Stock * storeInfo.CostoUnitario + numberOfUnits * localAmount) / (storeInfo.Stock + numberOfUnits), 2);
            if (ItemDataManager.GetInstance().UpdateUnitCost(updatedItem, storeInfo.StoreRoom, newCost))
            {
                storeInfo.CostoUnitario = newCost;
                return true;
            }
            else return false;
        }

        /// <summary>
        /// Actualiza la cantidad de unidades de un artículo en una bodega que hay en Stock.
        /// Lo que hace es sumar una cantidad al Stock actual, esta cantidad puede ser negativa si hay una salida
        /// </summary>
        /// <param name="itemId"></param>
        /// <param name="storeRoomId"></param>
        /// <param name="addedAmount"></param>
        public bool UpdateRequestedUnits(int itemId, int storeRoomId, int addedAmount)
        {
            Item updatedItem = GetItem(itemId);
            ItemStoreRoomInfo storeInfo = updatedItem.GetStoreRoomInfo(storeRoomId);
            if (ItemDataManager.GetInstance().UpdateStockAmount(updatedItem, storeInfo.StoreRoom, addedAmount))
            {
                storeInfo.Stock += addedAmount;
                return true;
            }
            else return false;
        }
        
        /// <summary>
        /// Obtiene la lista de unidades de medida
        /// </summary>
        /// <returns></returns>
        public List<MeasurementUnit> GetMeasurementUnits()
        {
            List<MeasurementUnit> measureList = new List<MeasurementUnit>();
            if (_unidMedida == null)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_UM_READING;
            }
            else
                foreach (MeasurementUnit measure in _unidMedida.Values)
                    measureList.Add(measure);
            return measureList;
        }

        /// <summary>
        /// Obtiene la unidad de medida con el Id dado
        /// </summary>
        /// <param name="measureId"></param>
        /// <returns></returns>
        public MeasurementUnit GetMeasurementUnit(int measureId)
        {
            return _unidMedida[measureId];
        }

        /// <summary>
        /// Verirfica si un codigo dado ya existe en la lista de items x bodega (que es la lista completa de items)
        /// </summary>
        /// <param name="code"> El codigo del que se desea verificar su existencia</param>
        /// <returns>Un valor de verdad correspondiente a la existencia del objeto</returns>
        public bool CheckCode(string code)
        {
            foreach (Item articulo in _items.Values)
            {
                if (articulo.CodArticulo == code)
                    return true;
            }

            return false; 
        }

        /// <summary>
        /// Metodo que regista un articulo en la bd y crea auno por bodega existente tambien
        /// </summary>
        /// <param name="itemCode">codigo articulo</param>
        /// <param name="Description"> descripcion </param>
        /// <param name="coment">comentario</param>
        /// <param name="um"> id de la unidad de medida</param>
        /// <returns>Valor de verdad correspondiente al exito del registro</returns>
        public bool RegisterItem(String codArticulo, String descripcion, int idUnidadMedida, String comentario)
        {
            int noErrorDB;
            Item newItem = new Item(codArticulo, descripcion, idUnidadMedida, comentario);

            if (!CheckCode(codArticulo))
            {
                noErrorDB = ItemDataManager.GetInstance().RegisterItem(newItem);
                if (noErrorDB == Constants.INVALID_ID)
                {
                    _IsError = true;
                    _ErrorMessage = Constants.ERROR_NEW_ITEM_FAILED;
                    return false;
                }

                newItem.Id = noErrorDB;
                _items.Add(newItem.Id, newItem);
                return true;
            }
            else
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_NEW_ITEM_CODE_FAILED;
                return false;
            }
        }

        /// <summary>
        /// Registra una imagen para un producto
        /// </summary>
        /// <param name="idArt"></param>
        /// <param name="imagen"></param>
        /// <returns></returns>
        public bool RegisterItemImage(int idArt, byte[] imagen)
        {
            bool resultOk;
            Item newItem = new Item(idArt, imagen);

            resultOk = ItemDataManager.GetInstance().RegisterItemImage(newItem);
            if (!resultOk)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_NEW_ITEM_FAILED;
                return false;
            }
            else
            {
                _IsError = false;
                _ErrorMessage = Constants.ERROR_NEW_ITEM_FAILED;
                return true;
            }           
            
        }

        /// <summary>
        /// Metodo que retorna la lista de imagenes de un articulo
        /// </summary>
        /// <returns></returns>
        public List<Item> GetItemImages(int idItem)
        {
            List<Item> items = new List<Item>();
            
            items = ItemDataManager.GetInstance().GetItemImages(idItem);

            if (items == null)
            {
                _IsError = true;
                _ErrorMessage = MessageConstants.ITEM_IMAGE_FAILED;
            }            

            return items;
        }

        /// <summary>
        /// Actualiza los valores de un item en la BD, tanto para el item, como sus cuentas en una bodega especifica
        /// </summary>
        /// <param name="idItem"></param>
        /// <param name="code"></param>
        /// <param name="description"></param>
        /// <param name="coment"></param>
        /// <param name="idum"></param>
        /// <param name="salesAcc"></param>
        /// <param name="purchasesAcc"></param>
        /// <param name="inventoryAcc"></param>
        /// <param name="salesCostAcc"></param>
        /// <param name="idStore"></param>
        /// <returns></returns>
        public bool UpdateItem(int idItem, string code, string description, string coment, int idum, int salesAcc, int purchasesAcc, int inventoryAcc, int salesCostAcc, int idStore)
        {
            Item updatedItem;
            CatalogManager catMan;
            Account sales;
            Account purchases;
            Account inventory;
            Account salesCost;
            bool ErrorDB;

            catMan = CatalogManager.GetInstance();
            sales = catMan.GetAccount(salesAcc);
            purchases = catMan.GetAccount(purchasesAcc);
            inventory = catMan.GetAccount(inventoryAcc);
            salesCost = catMan.GetAccount(salesCostAcc);
            updatedItem = _items[idItem];
            updatedItem.Id = idItem;
            updatedItem.CodArticulo = code;
            updatedItem.Descripción = description;
            updatedItem.UnidadMedida = idum;
            updatedItem.Comentario = coment;
            ItemStoreRoomInfo storeInfo = updatedItem.GetStoreRoomInfo(idStore);
            catMan = CatalogManager.GetInstance();
            storeInfo.CuentaInventario = inventory;
            storeInfo.CuentaVentas = sales;
            storeInfo.CuentaCompras = purchases;
            storeInfo.CuentaCostoVentas = salesCost;

            ErrorDB = ItemDataManager.GetInstance().UpdateItem(updatedItem, storeInfo);
            if (!ErrorDB)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_NEW_ITEM_FAILED;
                return false;
            }
            return true;                        
        }

        /// <summary>
        /// Metodo que retorna la lista de articulos registrados en el sistema
        /// </summary>
        /// <returns></returns>
        public List<Item> GetItems()
        {
            List<Item> items = new List<Item>();
            if (_items == null)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_ARTICLE_READING;
                return null;
            }
            else
                foreach (Item item in _items.Values)
                    items.Add(item);
            return items;
        }

        /// <summary>
        /// Obtiene el artículo con el Id dado
        /// </summary>
        /// <param name="itemId"></param>
        /// <returns></returns>
        public Item GetItem(int itemId)
        {
            return _items[itemId];
        }

        /// <summary>
        /// Obtiene el artículo con  el código dado
        /// </summary>
        /// <param name="itemCode">Codigo del artículo</param>
        /// <returns>Objeto Artículo</returns>
        public Item GetItem(String itemCode)
        {
            foreach (Item item in this.GetItems())
                if (item.CodArticulo == itemCode)
                    return item;
            return null;
        }

        /// <summary>
        /// Borra el artículo con el Id dado
        /// </summary>
        /// <param name="idItem"></param>
        /// <returns></returns>
        public bool DeleteItem(int idItem)
        {
            bool ErrorDB;

            ErrorDB = ItemDataManager.GetInstance().DeleteItem(idItem);

            if (!ErrorDB)
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_DELETE_ITEM_FAILED;
                return false;
            }
            _items.Remove(idItem);
            return true;
        }

        public bool MakeInventoryInputMovement(int itemId, int storeRoomId, double localAmountCost, int itemAmount, int accountId, int auxAccountId, string documentType)
        {
            bool result = makeInventoryIOMovement(itemId, storeRoomId, localAmountCost, itemAmount, accountId, Constants.InventoryMovementType.entry, auxAccountId, documentType);
            if (result && documentType == Constants.IE_ACCOUNTING_ENTRY)
                DocumentTypeManager.GetInstance().IncreaseDocumentConsecutiveNumber(documentType);
            return result;
        }

        public bool MakeInventoryOutputMovement(int itemId, int storeRoomId, int itemAmount, int accountId, int auxAccountId, string documentType)
        {
            double localAmountCost = GetItem(itemId).GetStoreRoomInfo(storeRoomId).CostoUnitario;
            bool result = makeInventoryIOMovement(itemId, storeRoomId, localAmountCost, itemAmount, accountId, Constants.InventoryMovementType.output, auxAccountId, documentType);
            if(result && documentType == Constants.II_ACCOUNTING_ENTRY)
                DocumentTypeManager.GetInstance().IncreaseDocumentConsecutiveNumber(documentType);
            return result;
        }

        /// <summary>
        /// Realiza un movimiento de entrada o salida de inventario
        /// </summary>
        /// <param name="itemId">Id del articulo para el que se está haciendo la operación</param>
        /// <param name="storeRoomId">Id de la bodega sobre la que se hace la operación</param>
        /// <param name="localAmountCost">Costo unitario en moneda local de cada artículo ingresado</param>
        /// <param name="itemAmount">Cantidad de artículos con dicho costo unitario</param>
        /// <param name="accountId">Id de la cuenta contra la que se hace el incremento o decremento de los artículos</param>
        /// <param name="movementType">Tipo de operación: entrada o salida</param>
        /// <param name="auxAccountId">Id de la cuenta auxiliar asociada al accountId. Si no posee puede ser un Constants.INVALID_ID</param>
        /// <param name="documentType">Tipo de documento sobre el que se está haciendo el movimiento de inventario</param>
        /// <returns></returns>
        private bool makeInventoryIOMovement(int itemId, int storeRoomId, double localAmountCost, int itemAmount, int accountId, Constants.InventoryMovementType movementType, int auxAccountId, string documentType)
        {
            List<AccountingEntryDetail> entryDetails;
            int stockFactor;
            Constants.AccountingEntryType inventoryType;
            Constants.AccountingEntryType accountType;
            ItemStoreRoomInfo storeInfo = GetItem(itemId).GetStoreRoomInfo(storeRoomId);
            // Verifico que tipo de movimiento es, si entrada o salida y acorde eso indico como va a hacer el asiento y actualizo el costo unitario
            if (movementType == Constants.InventoryMovementType.entry)
            {
                inventoryType = Constants.AccountingEntryType.debit;
                accountType = Constants.AccountingEntryType.credit;
                stockFactor = 1;
                if (!UpdateUnitCost(itemId, storeRoomId, localAmountCost, itemAmount))
                {
                    _IsError = true;
                    _ErrorMessage = Constants.ERROR_UPDATE_UNIT_COST;
                    return false;
                }
            }
            else
            {
                if (storeInfo.Stock < itemAmount)
                {
                    _IsError = true;
                    _ErrorMessage = Constants.NO_ENOUGH_UNITS;
                    return false;
                }
                stockFactor = -1;
                inventoryType = Constants.AccountingEntryType.credit;
                accountType = Constants.AccountingEntryType.debit;
            }
            double localEntryAmount = Math.Round(localAmountCost * itemAmount, 2);
            double systemEntryAmount = Math.Round(localEntryAmount/MoneyManager.GetInstance().SystemCurrency.ToLocalCurrency, 2);

            entryDetails = new List<AccountingEntryDetail>();
            entryDetails.Add(AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                storeInfo.CuentaInventario.AccountID, systemEntryAmount, localEntryAmount, inventoryType, Constants.INVALID_ID));

            entryDetails.Add(AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                accountId, systemEntryAmount, localEntryAmount, accountType, auxAccountId));

            if (!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(
                documentType, DateTime.Now, DateTime.Now, PeriodManager.GetInstance().GetCurrentAccountingPeriod().GetCurrentSeason().Id,
                "Código " + DocumentTypeManager.GetInstance().GetType(documentType).CurrentNumber.ToString(), "", "", entryDetails))
            {
                _IsError = true;
                _ErrorMessage = AccountingEntryManager.GetInstance().ErrorMessage;
                return false;
            }
            if(!UpdateRequestedUnits(itemId, storeRoomId, stockFactor*itemAmount))
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_UPDATE_ADDED_STOCK;
                return false;
            }
            return true;
        }

        /// <summary>
        /// Transferencia de bodega a bodega, hace asientos, actualización de costo unitario y actualización de artículos en stock
        /// </summary>
        /// <param name="itemId"></param>
        /// <param name="inputStoreRoomId"></param>
        /// <param name="outputStoreRoomId"></param>
        /// <param name="itemAmount"></param>
        /// <returns></returns>
        public bool MakeItemTransfer(int itemId, int inputStoreRoomId, int outputStoreRoomId, int itemAmount)
        {
            List<AccountingEntryDetail> entryDetails;
            ItemStoreRoomInfo inputStoreInfo = GetItem(itemId).GetStoreRoomInfo(inputStoreRoomId);
            ItemStoreRoomInfo outputStoreInfo = GetItem(itemId).GetStoreRoomInfo(outputStoreRoomId);
            // Verifico que haya suficientes unidades para la transferencia
            if (outputStoreInfo.Stock < itemAmount)
            {
                _IsError = true;
                _ErrorMessage = Constants.NO_ENOUGH_UNITS;
                return false;
            }
            // Hago el asiento contable
            double localEntryAmount = outputStoreInfo.CostoUnitario * itemAmount;
            double systemEntryAmount = Math.Round(localEntryAmount / MoneyManager.GetInstance().SystemCurrency.ToLocalCurrency, 2);

            entryDetails = new List<AccountingEntryDetail>();
            entryDetails.Add(AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                inputStoreInfo.CuentaInventario.AccountID, systemEntryAmount, localEntryAmount, Constants.AccountingEntryType.debit, Constants.INVALID_ID));

            entryDetails.Add(AccountingEntryManager.GetInstance().BuildAccountingEntryDetail(
                outputStoreInfo.CuentaInventario.AccountID, systemEntryAmount, localEntryAmount, Constants.AccountingEntryType.credit, Constants.INVALID_ID));

            if (!AccountingEntryManager.GetInstance().CreateAndRecordAccountingEntry(
                Constants.AS_ACCOUNTING_ENTRY, DateTime.Now, DateTime.Now, PeriodManager.GetInstance().GetCurrentAccountingPeriod().GetCurrentSeason().Id, Constants.INVENTORY_TRANSFER_REFERENCE, "", "", entryDetails))
            {
                _IsError = true;
                _ErrorMessage = AccountingEntryManager.GetInstance().ErrorMessage;
                return false;
            }

            // Actualizamos el costo unitario de la bodega de entrada
            if (!UpdateUnitCost(itemId, inputStoreRoomId, outputStoreInfo.CostoUnitario, itemAmount))
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_UPDATE_UNIT_COST;
                return false;
            }

            // Actualizamos la cantidad en Stock de ambas bodegas
            if (!UpdateRequestedUnits(itemId, inputStoreRoomId, itemAmount) || !UpdateRequestedUnits(itemId, outputStoreRoomId, -itemAmount))
            {
                _IsError = true;
                _ErrorMessage = Constants.ERROR_UPDATE_ADDED_STOCK;
                return false;
            }
            return true;
        }

        #endregion        
    }
}
